新特性
- 向后兼容
- 新特性
- breaking change
- 性能提升
- 大小 -41%
- 初次渲染快 +51%
- 更新 +133%
- 内存减少 -54%
- typescript 支持
Composition API
为什么
随着功能的增长,复杂组件代码难以维护
Vue2 api 通过一些列的 object 组织代码,缺少一种比较干净的在多个组件之间提取和复用的机制
// vue2 的实现一个复杂功能,代码可能会很分散 export default { data { // 复杂功能1的数据 // 复杂功能2的数据 }, methods: { // 复杂功能1的实现方法 // 复杂功能2的实现方法 }, computed: { // 复杂功能1 // 复杂功能2 }, filters: { // ... } } // 修改一个功能,可能就要在文件中修改 data, methods, computed, 甚至 filter 等
使用 mixin 解决复用的问题
- 不知道mixin暴露的对象中的数据是什么,方法是什么、返回是什么。对象是有一定封闭性的。
- mixin中同样会有 data、computed、methods等,逻辑依然会分散
- 命名冲突导致覆盖问题
- 不知道mixin暴露的对象中的数据是什么,方法是什么、返回是什么。对象是有一定封闭性的。
vue2 对 Typescript 的支持有局限
解决方法,使用函数
- 按逻辑组织代码,一个功能对应一个函数,代码组织更集中
- 在普通情况下,函数使用的参数、返回值更易查找。
- 在 ts 的加持下,函数参数和返回值的提示更友好。
setup
不用像 vue2 那样,写 data, computed, methods 等分散的逻辑。而将这些写在 setup 方法中。setup
在props, data, computed, methods, 生命周期函数运行之前运行的,不能获得 this。
import { defineComponent } from 'vue'
export default defineComponent ({
setup() {
return ;
}
});
ref 生成响应式变量
<template>
<h1>
{{ count }}
</h1>
<a herf="javascript:;" @click="addCount">add</a>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent ({
setup() {
const count = ref(0);
const addCount = () => {
count.value++; // 使用引用类型,修改一个,其他也会更新
}
return {
count,
addCount
};
}
});
</script>
computed 计算属性
<template>
<h1>{{ count }}</h1>
<p>{{ double }}</p>
<a herf="javascript:;" @click="addCount">add</a>
</template>
<script>
import { defineComponent, ref, computed } from 'vue'
export default defineComponent ({
setup() {
const count = ref(0); // 返回 ref 对象
const double = computed(() => count.value * 2); // double 是 computedRef 对象
return {
count,
double
};
}
});
</script>
reactive 生成响应式对象
<template>
<p>Name: {{ person.name }}</p>
<p>Age: {{ person.age }}</p>
<a herf="javascript:;" @click="person.change">change name</a>
</template>
<script>
import { defineComponent, reactive } from 'vue'
export default defineComponent ({
setup() {
const person = reactive({
name: 'viking',
age: 20,
change() {
person.name = 'maomao';
person.age = 30;
}
})
return {
person
};
}
});
</script>
- 注意:将 reactive 后的对象属性取出,会丧失响应性
toRefs
接受 reactive 对象,返回新的对象,但对象的每一项属性,都变成了 ref 类型实例(响应式的)
<template>
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
<a herf="javascript:;" @click="change">change name</a>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue'
export default defineComponent ({
setup() {
const person = reactive({
name: 'viking',
age: 20,
change() {
person.name = 'maomao';
person.age = 30;
}
});
const person2 = toRefs(person);
return {
...person2
};
}
});
</script>
生命周期
也是在 setup
中使用
beforeCreate
在 setup
之前运行,created
是在 setup
后运行,可以直接把 beforeCreate
和 created
中的逻辑,写在 setup
之中。所以这两个生命周期不再需要
深入响应式对象
- 保存未来执行修改的代码(effect)
- 监测值得改变
- 使用
proxy
对象实现
- 使用
- 值改变后,执行 trigger effect
监测值得改变
使用 proxy
拦截对目标对象的读取:
const person = {
name: 'Jone'
}
const handler = {
// 目标对象,属性名,代理对象
get(target, prop, receiver) {
console.log('trigger get')
return target[prop]
}
// 目标对象,属性名,要填入的值,代理对象
set(target, prop, value, receiver){
console.log('trigger set')
traget[prop] = value
return true // 告诉设置成功
}
}
const proxy = new Proxy(person, handler)
使用 Reflect
对象上的静态方法改写:
const person = {
name: 'Jone'
}
const handler = {
get() {
console.log('trigger get')
return Reflect.get(...arguments) // Reflect.get() 接收的参数和 handler.get 接收的参数一样
}
set(){
console.log('trigger set')
return Reflect.set(...arguments) // Reflect.set() 接收的参数和 handler.set 接收的参数一样
}
}
const proxy = new Proxy(person, handler)
上面两种方式,执行效果一样
存储和触发effect
- 将所有 effect 加入特定的数据结构
- 创建特定的函数可以再次运行这些 effect
- 使用 Proxy 的 getter 和 setter,将这些函数放入对应的位置
let product = { price: 5, count: 2 }
let total = 0
let dep = new Set()
function track() {
dep.add(effect)
}
function trigger() {
dep.forEach(effect => effect())
}
const reactive = (obj) => {
const handler = {
get() {
let result = Reflect.get(...arguments)
track()
return result
},
set() {
let result = Reflect.set(...arguments)
trigger()
return result
}
}
return new Proxy(obj, handler)
}
const product = reactive({ price: 5, count: 2 })
let effect = () => {
total = product.price * product.count
}
console.log(total)
product.price = 10
console.log(`total is ${total}`)
副作用
纯函数
相同的输入,永远会得到相同的输出
没有副作用
const double = x => x*2; // 给定一个 x ,输出的值永远都是一样的 Math.random(); // 不是一个纯函数
副作用
函数外部环境发生的交互
- 网络请求
- DOM 操作
- 订阅数据来源
- 写入文件系统
- 获取用户输入
Vue 副作用处理
import { defineComponent, watchEffect } from 'vue'
export default defineComponent {
props: {
msg: string
}
setup(props) {
watchEffect(() => {
console.log('props effect', props.msg);
})
}
}
- 组件第一次初始化时,会触发
watchEffect
watchEffect
回调里面的值,没有发生变化,就不会触发副总用
深入 watchEffect
自动收集依赖且触发
自动销毁 effect
在 setup 或生命周期钩子函数中使用
watchEffect
,在组件销毁时,副作用也会一起销毁可以手动清除
const stop = watchEffect(() => { console.log('props effect', props.msg); }); stop(); // 销毁
使副作用失效
watchEffect
中发送请求时,多次变化,势必多次请求watchEffect
向回调中提供参数,以停止未完成的副作用watchEffect((onInvalidate) => { console.log('props effect', props.msg); console.log('inner effect', count.value); const source = axios.CancelToken.source() axios.get(`https://jsonplaceholder.typicode.com/todos/${count.value}`, { cancelToken: source.token }).catch(err => { console.log(err.message); }); onInvalidate(() => { source.cancel('trigger'); }); });
副作用执行顺序
watchEffect
都是异步执行的watchEffect
先执行DOM updated
再执行<template> <h1 ref="node"></h1> <h1></h1> <a herf="javascript:;" @click="count++">change</a> </template> <script> import { defineComponent, ref } from 'vue' export default defineComponent ({ props: { msg: string } setup() { const node = ref<null | HTMLElement>(null); watchEffect(() => { const currentText = node.value ? node.value.innerText : ''; console.log(currentText); }, { flush: 'post' // 默认是 pre:之前, }) return { node }; } }); </script>
watchEffect
提供参数flush
改变执行顺序。post
表示,在DOM updated
之后再执行watchEffect
React 的执行顺序不可以调整,都是在组件 updated 之后触发
watch 精准控制 effect
<template>
<h1 ref="node">{{msg}}</h1>
<h1>{{count}}</h1>
<a herf="javascript:;" @click="count++">change</a>
</template>
<script>
import { ref, watch, toRefs } from 'vue'
export default defineComponent ({
props: {
msg: string
}
setup(props) {
const node = ref<null | HTMLElement>(null);
// 基本使用
watch(count, (newV, oldV) => {
console.log(count.value)
});
// 响应式对象的值
// watch(props.msg, (newV, oldV) => {});// props 是只读的,拿出其中的值,这个值将不再是响应式对象
// 方法 1
const { msg } = toRefs(props.msg);
watch(msg, (newV, oldV) => {});
// 方法 2
watch(() => props.msg, (newV, oldV) => {});
// watch 多个值
watch([() => props.msg, count] => props.msg, (newV, oldV) => {
console.log(newV.value) // [第一个值,第二个值]
});
return {
node
};
}
});
watch 的基本用法
watch(count, (newV, oldV) => {
console.log(count.value)
});
watch 响应式对象的单个值
- 使用 toRefs
- 使用 getter 函数
// watch(props.msg, (newV, oldV) => {});// props 是只读的,拿出其中的值,这个值将不再是响应式对象
// 方法 1
const { msg } = toRefs(props.msg);
watch(msg, (newV, oldV) => {});
// 方法 2
watch(() => props.msg, (newV, oldV) => {});
watch 多个值
- 使用数组
// watch 多个值
watch([() => props.msg, count] => props.msg, (newV, oldV) => {
console.log(newV.value) // [第一个值,第二个值]
});
和 watchEffect 对比
- 懒执行副作用:针对某个值执行副作用
- watch 可以定义什么状态应该触发 watcher 重新运行
- watch 可以访问数据变化前后的值,watchEffect 不能
自定义函数 - hooks
- 将相关的 feature 组合在一起
非常易于重用
界面的需求 - 转化为数据的描述
优点
- 以函数的形式调用,清楚的了解参数和返回的类型,更好的提示
- 避免命名冲突
- 代码逻辑脱离组件存在
示例
<template>
<div v-if="todo.loading">Loading todo</div>
<div v-else>{{ todo.result && todo.result.title }}</div>
</template>
<script lang="ts">
import useURLLoader from '../hooks/useURLLoader'
interface PostProps {
userId: number,
id: number,
title: string,
body: string
}
export default {
props: {
msg: String
},
setup() {
const todo = useURLLoader<PostProps>('url');
todo.result
return {
todo
}
}
}
</script>
<style scoped>
</style>
// useURLLoader.js
import { reactive } from 'vue'; // 响应式对象
import axios from 'axios';
interface DataProps<T> {
result: T | null,
loading: boolean,
loaded: boolean,
error: any
}
const useURLLoader = <T = any>(url: string) => {
const data = reactive<DataProps<T>>({
result: null,
loading: true,
loaded: false,
error: null
})
axios.get(url).then(resp => {
data.result = resp.data;
data.loaded = true;
}).catch(e => {
data.error = e;
}).finally(() => {
data.loaded = false;
});
return data;
};
export default useURLLoader;
对比 React 的自定义 hooks
- 更新数据的方式
- 触发的时机
- 为什么要包裹在 useEffect 中?
- 删除了会有什么问题?
- 为什么 Vue3 不需要这样做也可以?
其他自学知识点
- Teleport
- Fragment
- Emits Components Options
- Global API 修改
- 语法糖
<srcipt setup>
<style vars>